// Copyright (c) Microsoft Corporation
// All rights reserved

namespace Microsoft.Windows.Controls.Specialized.TextModel
{
    using System;
    using System.Diagnostics;
    using System.Collections.Generic;

    /// <summary>
    /// Defines a tracking Text Point on a Text Buffer
    /// </summary>
    public class TextPoint: IEquatable<TextPoint>
    {
        #region Private Members

        private TrackingMode _trackingMode;
        private ITextBuffer _textBuffer;
        private ITextVersion _version;
        private int _position;

        #endregion // Private Members

        /// <summary>
        /// Creates a Text point at the specified position and with the given tracking mode
        /// </summary>
        /// <param name="buffer">
        /// The Text Buffer with which this text point is associated
        /// </param>
        /// <param name="version">
        /// The Text buffer version that is associated with the specified position
        /// </param>
        /// <param name="position">
        /// The current position of the point
        /// </param>
        /// <param name="trackingMode">
        /// The tracking point that defines the behavior of tracking for this point
        /// </param>
        public TextPoint(ITextBuffer buffer, ITextVersion version, int position, TrackingMode trackingMode)
        {
            // Null validation
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (version == null)
                throw new ArgumentNullException("version");

            // Validate:  Make sure the supplied version is that of the buffer
            if (!buffer.VersionBelongsToBuffer(version))
                throw new ArgumentException("The specified version doesn't belong to the specified buffer");
            
            // Validate the position
            if (position < 0 || position > buffer.GetLengthOfVersion(version))
                throw new ArgumentOutOfRangeException("position");

            Initialize(buffer, version, position, trackingMode);
        }

        /// <summary>
        /// Creates a Text point at the specified position and with the given tracking mode
        /// </summary>
        /// <param name="buffer">
        /// The Text Buffer with which this text point is associated
        /// </param>
        /// <param name="position">
        /// The current position of the point
        /// </param>
        /// <param name="trackingMode">
        /// The tracking point that defines the behavior of tracking for this point
        /// </param>
        public TextPoint (ITextBuffer buffer, int position, TrackingMode trackingMode)
        {
            // Validation
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (position < 0 || position > buffer.Length)
                throw new ArgumentOutOfRangeException("position");

            Initialize(buffer, buffer.Version, position, trackingMode);
        }

        /// <summary>
        /// Create a text point at the specified line and character offset, with the tracking mode given
        /// </summary>
        /// <param name="buffer">
        /// The Text Buffer with which this text point is associated
        /// </param>
        /// <param name="line">
        /// The line that identifies the position
        /// </param>
        /// <param name="offset">
        /// The offset of the position in the line
        /// </param>
        /// <param name="trackingMode">
        /// The tracking point that defines the behavior of tracking for this point
        /// </param>
        public TextPoint(ITextBuffer buffer, int line, int offset, TrackingMode trackingMode)
        {
            // Null validation
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (line < 0 || line >= buffer.LineCount)
                throw new ArgumentOutOfRangeException("line");
            if (offset < 0 || offset > buffer.GetLengthOfLine(line))
                throw new ArgumentOutOfRangeException("offset");

            int position = buffer.GetStartOfLine(line) + offset;
            Initialize(buffer, buffer.Version, position, trackingMode);
        }

        /// <summary>
        /// Create a text point at the specified character position, with a default tracking mode
        /// </summary>
        /// <param name="buffer">
        /// The Text Buffer with which this text point is associated
        /// </param>
        /// <param name="position">
        /// The current position of the point
        /// </param>
        public TextPoint (ITextBuffer buffer, int position)
        {
            // Null validation
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (position < 0 || position > buffer.Length)
                throw new ArgumentOutOfRangeException("position");

            Initialize(buffer, buffer.Version, position, TrackingMode.Positive);
        }

        /// <summary>
        /// Create a text point at the specified line and character offset, with a default tracking mode
        /// </summary>
        /// <param name="buffer">
        /// The Text Buffer with which this text point is associated
        /// </param>
        /// <param name="line">
        /// The line that identifies the position
        /// </param>
        /// <param name="offset">
        /// The offset of the position in the line
        /// </param>
        public TextPoint (ITextBuffer buffer, int line, int offset)
        {
            // Null validation
            if (buffer == null)
                throw new ArgumentNullException("buffer");
            if (line < 0 || line >= buffer.LineCount)
                throw new ArgumentOutOfRangeException("line");
            if (offset < 0 || offset > buffer.GetLengthOfLine(line))
                throw new ArgumentOutOfRangeException("offset");

            int position = buffer.GetStartOfLine(line) + offset;
            Initialize(buffer, buffer.Version, position, TrackingMode.Positive);
        }

        /// <summary>
        /// Construction helper
        /// </summary>
        private void Initialize(ITextBuffer buffer, ITextVersion bufferVersion, int position, TrackingMode trackingMode)
        {
            _version = bufferVersion;
            _textBuffer = buffer;
            _position = position;
            _trackingMode = trackingMode;
        }

        #region Exposed Properties

        /// <summary>
        /// Gets the tracking mode for the Text Point
        /// </summary>
        public TrackingMode TrackingMode 
        { 
            get 
            { 
                return _trackingMode; 
            } 
        }

        /// <summary>
        /// Gets the Text Buffer with which this point is associated
        /// </summary>
        /// <value></value>
        public ITextBuffer TextBuffer 
        { 
            get 
            { 
                return _textBuffer; 
            } 
        }

        /// <summary>
        /// Gets the current position of this text point
        /// </summary>
        public int Position 
        { 
            get 
            { 
                Update (); 
                return _position; 
            } 
        }

        /// <summary>
        /// Gets the line number on which this text point is located
        /// </summary>
        public int Line 
        { 
            get 
            { 
                Update (); 
                return _textBuffer.GetLineContaining (_position); 
            } 
        }

        /// <summary>
        /// Gets the line offset for this text point
        /// </summary>
        public int LineOffset 
        { 
            get 
            { 
                Update (); 
                return _textBuffer.GetColumnOffset (_position); 
            } 
        }

        #endregion // Exposed Properties

        #region Exposed Methods

        /// <summary>
        /// Gets the position in the buffer at a specified version.
        /// </summary>
        /// <param name="version">
        /// The version corresponding to which the position is requested
        /// </param>
        /// <returns>
        /// An integer position
        /// </returns>
        /// <remarks>
        /// If the supplied version is older than the currently cached version (which is not exposed), then
        /// the position returned may not be accurate.  This is because when text changes are lossy.  For 
        /// example, if a text point is in between a deletion span, the text point is moved to the start of the
        /// deletion span.  It's not possible to go back to the exact point just by knowing all information
        /// about the deletion.
        /// </remarks>
        public int MapPositionTo(ITextVersion version)
        {
            // Validate
            if (version == null)
                throw new ArgumentNullException("version");

            if (!_textBuffer.VersionBelongsToBuffer(version))
                throw new ArgumentException("The specified version doesn't belong to this text buffer.");

            if (version.VersionIndex == _version.VersionIndex)
            {
                return _position;
            }
            else if (version.VersionIndex < _version.VersionIndex)
            {
                return GetPositionBackwardsInTime(version);
            }
            else
            {
                return GetPositionForwardsInTime(version);
            }
        }

        #endregion // Exposed Methods

        #region Private Helpers

        /// <summary>
        /// Updates the position based on changes that happened from the last updated version
        /// </summary>
        private void Update()
        {
            GetPositionForwardsInTime(_textBuffer.Version);
        }

        /// <summary>
        /// Gets the position of the text point at a version that's later than the currently cached version
        /// </summary>
        /// <param name="version">
        /// The version at which the position is requested
        /// </param>
        /// <returns>
        /// An integer position
        /// </returns>
        /// <remarks>
        /// Since it's always better to track the buffer as close as possible, this method will actually modify
        /// the cached version to match the specified version.
        /// </remarks>
        private int GetPositionForwardsInTime(ITextVersion version)
        {
            // Walk through the text change list and apply the change in position
            while (_version != version)
            {
                Debug.Assert(_version.Next != null);
                if (_version.Change.Position < _position ||
                    (_trackingMode == TrackingMode.Positive && _version.Change.Position == _position))
                {
                    if (_version.Change.OldEnd < _position)
                    {
                        _position += _version.Change.Delta;
                    }
                    else
                    {
                        if (_trackingMode == TrackingMode.Negative)
                            _position = _version.Change.Position;
                        else
                            _position = _version.Change.NewEnd;
                    }
                }

                _version = _version.Next;
            }

            return _position;
        }

        /// <summary>
        /// Gets the position of the text point at a version that's earlier than the currently cached version
        /// </summary>
        /// <param name="version">
        /// The version at which the position is requested
        /// </param>
        /// <returns>
        /// An integer position
        /// </returns>
        private int GetPositionBackwardsInTime(ITextVersion version)
        {
            // We'll have to work our way backwards.  So, let's first push items on the stack
            Stack<TextChange> textChanges = new Stack<TextChange>();
            while (version != _version)
            {
                textChanges.Push(version.Change);
                version = version.Next;
            }

            int position = _position;
            while (textChanges.Count > 0)
            {
                TextChange textChange = textChanges.Pop();
                if (textChange.Position < position)
                {
                    position -= textChange.Delta;
                }
            }

            return position;
        }

        #endregion // Private Helpers

        #region Static Operator Overloads

        public static implicit operator int (TextPoint point)
        {
            return point.Position;
        }

        public static bool operator == (TextPoint p1, TextPoint p2)
        {
            if (p1 == (object)null && p2 == (object)null)
                return true;

            if (p1 == (object)null || p2 == (object)null)
                return false;

            return p1.Position == p2.Position;
        }

        public static bool operator != (TextPoint p1, TextPoint p2)
        {
            return !(p1 == p2);
        }

        public static bool operator > (TextPoint p1, TextPoint p2)
        {
            return p1.Position > p2.Position;
        }

        public static bool operator < (TextPoint p1, TextPoint p2)
        {
            return p1.Position < p2.Position;
        }

        public static bool operator >= (TextPoint p1, TextPoint p2)
        {
            return p1.Position >= p2.Position;
        }

        public static bool operator <= (TextPoint p1, TextPoint p2)
        {
            return p1.Position <= p2.Position;
        }

        public static int Compare (TextPoint p1, TextPoint p2)
        {
            return p1.Position - p2.Position;
        }

        public override bool Equals (object obj) 
        {
            if (obj == null || (!(obj is TextPoint)))
                return false;

            return this == (TextPoint)obj;
        }

        #endregion // Static Operator Overloads

        #region Overrides

        public override int GetHashCode()
        {
            return Position;
        }

        public override string ToString () 
        { 
            return string.Format(System.Globalization.CultureInfo.CurrentCulture, "({0})", this.Position);
        }

        #endregion // Overrides

        #region IEquatable<TextPoint> Members

        public bool Equals(TextPoint other) {
            return this == other;
        }

        #endregion
    }
}
